package com.leyou.search.service.impl;

import com.leyou.common.dto.PageDTO;
import com.leyou.common.exception.LyException;
import com.leyou.item.client.ItemClient;
import com.leyou.item.dto.*;
import com.leyou.search.constants.SearchConstants;
import com.leyou.search.dto.SearchParamDTO;
import com.leyou.search.entity.Goods;
import com.leyou.search.repository.GoodsRepository;
import com.leyou.search.service.SearchService;
import com.leyou.starter.elastic.entity.PageInfo;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.nested.Nested;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.*;
import java.util.stream.Collectors;

import static com.leyou.search.constants.SearchConstants.*;

/**
 * @ClassName : SearchServiceImpl  //类名
 * @Description :   //描述
 * @Author : qingfeng  //作者
 * @Date: 2020-09-19 19:11  //时间
 */
@Service
public class SearchServiceImpl implements SearchService {

    private final GoodsRepository goodsRepository;
    private final ItemClient itemClient;

    public SearchServiceImpl(GoodsRepository goodsRepository, ItemClient itemClient) {
        this.goodsRepository = goodsRepository;
        this.itemClient = itemClient;
    }

    @Override
    public void createIndexAndMapping() {
        //先删除以前的，避免冲突
        try {
            goodsRepository.deleteIndex();
        } catch (Exception e) {
            e.printStackTrace();
        }
        //然后创建一个新的
        goodsRepository.createIndex("{\n" +
                "  \"template\": \"goods_suggest_template\",\n" +
                "  \"settings\": {\n" +
                "    \"analysis\": {\n" +
                "      \"analyzer\": {\n" +
                "        \"my_pinyin\": {\n" +
                "          \"tokenizer\": \"ik_smart\",\n" +
                "          \"filter\": [\n" +
                "            \"py\"\n" +
                "          ]\n" +
                "        }\n" +
                "      },\n" +
                "      \"filter\": {\n" +
                "        \"py\": {\n" +
                "\t\t  \"type\": \"pinyin\",\n" +
                "          \"keep_full_pinyin\": true,\n" +
                "          \"keep_joined_full_pinyin\": true,\n" +
                "          \"keep_original\": true,\n" +
                "          \"limit_first_letter_length\": 16,\n" +
                "          \"remove_duplicated_term\": true\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  },\n" +
                "  \"mappings\": {\n" +
                "    \"properties\": {\n" +
                "      \"id\": {\n" +
                "        \"type\": \"keyword\"\n" +
                "      },\n" +
                "      \"suggestion\": {\n" +
                "        \"type\": \"completion\",\n" +
                "        \"analyzer\": \"my_pinyin\",\n" +
                "        \"search_analyzer\": \"ik_smart\"\n" +
                "      },\n" +
                "      \"title\":{\n" +
                "        \"type\": \"text\",\n" +
                "        \"analyzer\": \"my_pinyin\",\n" +
                "        \"search_analyzer\": \"ik_smart\"\n" +
                "      },\n" +
                "      \"image\":{\n" +
                "        \"type\": \"keyword\",\n" +
                "        \"index\": false\n" +
                "      },\n" +
                "      \"updateTime\":{\n" +
                "        \"type\": \"date\"\n" +
                "      },\n" +
                "      \"specs\":{\n" +
                "        \"type\": \"nested\",\n" +
                "        \"properties\": {\n" +
                "          \"name\":{\"type\": \"keyword\" },\n" +
                "          \"value\":{\"type\": \"keyword\" }\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  }\n" +
                "}");
    }

    @Override
    public void loadData() {
        int page = 1, rows = 100;
        while (true) {
            // 分页查询已经上架的spu
            PageDTO<SpuDTO> result = itemClient.querySpuByPage(page, rows, true, null, null, null);
            List<SpuDTO> list = result.getItems();

            // 遍历Spu集合，把SpuDTO通过buildGoods方法转为Goods
            List<Goods> goodsList = list.stream().map(this::buildGoods).collect(Collectors.toList());

            // 批量写入Elasticsearch
            goodsRepository.saveAll(goodsList);

            // 翻页
            page++;

            // 获取总页数
            Long totalPage = result.getTotalPage();
            // 判断是否还有spu没有查询
            if(page > totalPage){
                // 没有则结束
                break;
            }
        }
    }

    @Override
    public Mono<List<String>> getSuggestion(String keyPrefix) {
        /**
         * @Description //TODO 查询自动补全的数据
         * @Param [key]
         * @return reactor.core.publisher.Mono<java.util.List < java.lang.String>>
         */
        //准备模板参数
//        HashMap<String, Object> params = new HashMap<>();
//        params.put(SUGGESTION_PARAM_PREFIX_KEY,key);
//        params.put(SUGGESTION_PARAM_FIELD,SUGGESTION_FIELD);
//        //准备
//        return goodsRepository.suggestByTemplate(SUGGESTION_TEMPLATE_ID,params);
        if (StringUtils.isBlank(keyPrefix)) {
            throw new LyException(400, "请求参数不能为空！");
        }
        return goodsRepository.suggestBySingleField(SUGGESTION_FIELD, keyPrefix);

    }

    @Override
    public Mono<PageInfo<Goods>> searchGoods(SearchParamDTO param) {
        /**
         * @Description //TODO 分页查询商品数据
         * @Param [request]
         * @return reactor.core.publisher.Mono<com.leyou.starter.elastic.entity.PageInfo < com.leyou.search.entity.Goods>>
         */
        //1.构建查询条件的工厂对象
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //1.1 source过滤
        sourceBuilder.fetchSource(DEFAULT_SOURCE_FIELD,new String[0]);
        //1.2 查询条件
        String key = param.getKey();
        if (StringUtils.isBlank(key)){
            //搜索条件为null，返回异常
            throw new LyException(400,"搜索条件不能为空!");
        }
        sourceBuilder.query(QueryBuilders.matchQuery(DEFAULT_SEARCH_FIELD,key));
        //1.3 分页条件
        sourceBuilder.from(param.getFrom());
        sourceBuilder.size(param.getSize());
        //1.4 排序条件
        if (StringUtils.isNoneBlank(param.getSortBy())){
            //排序字段存在，才去排序
            sourceBuilder.sort(param.getSortBy(),param.getDesc()? SortOrder.DESC:SortOrder.ASC);
        }
        //1.5 高亮条件
        sourceBuilder.highlighter(new HighlightBuilder()
                .field(DEFAULT_SEARCH_FIELD)
                .preTags(DEFAULT_PRE_TAG)
                .postTags(DEFAULT_POST_TAG));
        return goodsRepository.queryBySourceBuilderForPageHighlight(sourceBuilder);
    }

    @Override
    public Mono<Map<String, List<?>>> getFilters(SearchParamDTO param) {
        /**
         * @Description //TODO 查询过滤项
         * @Param [param]
         * @return reactor.core.publisher.Mono<java.util.Map < java.lang.String, java.util.List < ?>>>
         */
        //1 构建查询条件的工厂对象
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        //1.1 查询条件
        String key = param.getKey();
        if (StringUtils.isBlank(key)) {
            //搜索条件为空，返回异常
            throw new LyException(400,"搜索条件不能为空！");
        }
        sourceBuilder.query(QueryBuilders.matchQuery(SearchConstants.DEFAULT_SEARCH_FIELD,key));
        //1.2 分页设置为0，不需要商品数据，只要聚合结果
        sourceBuilder.size(0);
        //2 添加聚合条件
        //2.1 分类的聚合
        sourceBuilder.aggregation(AggregationBuilders.terms("categoryAgg").field(CATEGORY_FIELD_NAME));
        //2.2 品牌的聚合
        sourceBuilder.aggregation(AggregationBuilders.terms("brandAgg").field(BRAND_FIELD_NAME));
        //2.3 规格参数nested聚合
        sourceBuilder.aggregation(
                //nested聚合
                AggregationBuilders.nested("specAgg",SPEC_FIELD_PATH)
                //嵌套名称的聚合
                .subAggregation(AggregationBuilders.terms("nameAgg").field(SPEC_NAME_FIELD_NAME)
                    //嵌套对value的聚合
                    .subAggregation(AggregationBuilders.terms("valueAgg").field(SPEC_VALUE_FIELD_NAME)))
        );
        //3 查询
        Mono<Aggregations> mono = goodsRepository.aggregationBySourceBuilder(sourceBuilder);
        //4 解析结果
        return mono.map(this::parseAggregationsResult);
    }

    @Override
    public void saveGoodsById(Long spuId) {
        /**
         * @Description //TODO 添加goods
         * @Param [spuId]
         * @return void
         */
        //需要spu
        SpuDTO spu = itemClient.queryGoodsById(spuId);
        //构建goods
        Goods goods = buildGoods(spu);
        //写入索引库
        goodsRepository.save(goods);
    }

    @Override
    public void deleteGoodsById(Long spuId) {
        /**
         * @Description //TODO 删除goods
         * @Param [id]
         * @return void
         */
        goodsRepository.deleteById(spuId);
    }

    private Map<String,List<?>> parseAggregationsResult(Aggregations aggregations) {
        //1 创建一个map结果
        LinkedHashMap<String, List<?>> filters = new LinkedHashMap<>();
        //2 解析分类聚合结果
        //2.1 根据名称获取聚合结果
        Terms categoryAgg = aggregations.get("categoryAgg");
        //2.2 获取buckets,从buckets中解析出分类id的集合
        List<Long> categoryIds = categoryAgg.getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());
        //2.3 根据分类的id查询分类的对象
        List<CategoryDTO> categories = itemClient.queryCategoryByIds(categoryIds);
        //2.4 存入filters
        filters.put("分类",categories);
        //3 解析品牌聚合结果
        //3.1 根据名称获取聚合结果
        Terms brandAgg = aggregations.get("brandAgg");
        //3.2 获取buckets，从buckets中解析出分类ID的集合
        List<Long> brandIds = brandAgg.getBuckets()
                .stream()
                .map(Terms.Bucket::getKeyAsNumber)
                .map(Number::longValue)
                .collect(Collectors.toList());
        //3.3 根据品牌的id查询品牌的对象
        List<BrandDTO> brands = itemClient.queryBrandByIds(brandIds);
        //3.4 存入filters
        filters.put("品牌",brands);
        //4 解析规格参数聚合结果
        //4.1 根据名称获取聚合结果
        Nested specAgg = aggregations.get("specAgg");
        //4.2 获取对规格参数名称的聚合结果
        Terms nameAgg = specAgg.getAggregations().get("nameAgg");
        //4.3 获取buckets,并且遍历
        for (Terms.Bucket bucket : nameAgg.getBuckets()) {
            //4.3.1 从bucket中获取key，就是规格参数的名称
            String specName = bucket.getKeyAsString();
            //4.3.2 获取当前规格参数的值的聚合结果
            Terms valueAgg = bucket.getAggregations().get("valueAgg");
            //4.3.3 从valueAgg中获取buckets，并解析其中的key
            List<String> values = valueAgg.getBuckets()
                    .stream()
                    .map(Terms.Bucket::getKeyAsString)
                    .collect(Collectors.toList());
            //4.3.4 放入filters
            filters.put(specName,values);
        }
        return filters;
    }

    private Goods buildGoods(SpuDTO spuDTO) {
        // 1.用于自动补全的信息，包括名称、分类名称、品牌名称
        String categoryName = StringUtils.substringAfterLast(spuDTO.getCategoryName(), "/");
        List<String> suggestion = Arrays.asList(spuDTO.getName(), spuDTO.getBrandName(), categoryName);
        // 2.规格参数信息
        // 2.1.查询规格参数数据
        List<SpecParamDTO> params = itemClient.querySpecValues(spuDTO.getId(), true);
        // 2.2.准备map，存放name和value
        List<Map<String, Object>> specs = new ArrayList<>();
        // 2.3.将param处理成map
        for (SpecParamDTO param : params) {
            Map<String, Object> spec = new HashMap<>();
            // 获取参数名称
            spec.put("name", param.getName());
            // 获取参数值
            spec.put("value", chooseSegment(param));
            // 放回specs
            specs.add(spec);
        }

        // 3.销量,spu下的每个sku的销量和
        // 3.1.查询spu下的sku
        List<SkuDTO> skuList = itemClient.querySkuBySpuId(spuDTO.getId());
        // 3.2.准备价格集合
        Set<Long> prices = new TreeSet<>();
        long sold = 0;
        for (SkuDTO sku : skuList) {
            // 3.3.计算销量和
            sold += sku.getSold();
            // 获取价格
            prices.add(sku.getPrice());
        }

        // 4.某个sku的图片
        String images = skuList.get(0).getImages();
        // 5.创建一个Goods对象，封装数据
        Goods goods = new Goods();
        goods.setUpdateTime(new Date());
        goods.setTitle(spuDTO.getTitle());
        goods.setCategoryId(spuDTO.getCid3());
        goods.setBrandId(spuDTO.getBrandId());
        goods.setId(spuDTO.getId());
        // 5.1.用于自动补全的信息
        goods.setSuggestion(suggestion);
        // 5.2.规格参数信息
        goods.setSpecs(specs);
        // 5.3.销量
        goods.setSold(sold);
        // 5.4.价格
        goods.setPrices(prices);
        // 5.5.某个sku的图片
        goods.setImage(StringUtils.substringBefore(images, ","));
        return goods;
    }

    private Object chooseSegment(SpecParamDTO p) {
        Object value = p.getValue();
        if (value == null || StringUtils.isBlank(value.toString())) {
            return "其它";
        }
        if(!p.getNumeric() || StringUtils.isBlank(p.getSegments()) || value instanceof Collection){
            return value;
        }
        double val = parseDouble(value.toString());
        String result = "其它";
        // 保存数值段
        for (String segment : p.getSegments().split(",")) {
            String[] segs = segment.split("-");
            // 获取数值范围
            double begin = parseDouble(segs[0]);
            double end = Double.MAX_VALUE;
            if (segs.length == 2) {
                end = parseDouble(segs[1]);
            }
            // 判断是否在范围内
            if (val >= begin && val < end) {
                if (segs.length == 1) {
                    result = segs[0] + p.getUnit() + "以上";
                } else if (begin == 0) {
                    result = segs[1] + p.getUnit() + "以下";
                } else {
                    result = segment + p.getUnit();
                }
                break;
            }
        }
        return result;
    }

    private double parseDouble(String str) {
        try {
            return Double.parseDouble(str);
        } catch (Exception e) {
            return 0;
        }
    }
}
